/*
 * ﻿Copyright (C) 2011-2014 NewMain Softech
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package com.newmainsoftech.aspectjutil.adviseutil;

import java.io.File;
import java.lang.management.ManagementFactory;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.sun.tools.attach.VirtualMachine;

/**
 * Loader for javaagent and dynamically attach it to running process.  
 * Note: Only supports Sun's JVM (currently).
 * 
 * @author <a href="mailto:artymt@gmail.com">Arata Y.</a>
 */
public class AgentLoader {
//TODO change logging system to use LoggerWrapper, or move this and AspectJWeaverAgentLoader to individual module
	Logger logger = Logger.getLogger( this.getClass().getName());
		Logger getLogger() {
			return logger;
		}

	public String getJvmSpecVersion() {
		return System.getProperty( "java.specification.version");
	}
	
	/**
	 * Check whether JVM's spec version is 1.5 or higher.
	 * 
	 * @return true when JVM's spec version is 1.5 or higher.
	 */
	public boolean isApplicableJavaVersion() {
	    String javaSpecVersion = getJvmSpecVersion();
		int counter = -1;
		boolean isApplicableVersion = false;
		for( String versionToken : javaSpecVersion.split( "\\.")) {
			if ( ++counter > 1) break; // for
			if ( counter == 0) {
				if ( Integer.parseInt( versionToken) < 1) break; //for
				if ( Integer.parseInt( versionToken) > 1) {
					isApplicableVersion = true;
					break; // for
				}
			}
			else {
				if ( Integer.parseInt( versionToken) > 4) {
					isApplicableVersion = true;
					break; // for
				}
			}
		} // for
		return isApplicableVersion;
	}
	
	/**
	 * Dynamically apply JavaAgent specified by <code>agentFilePath</code> input to current "PROCESS"  
	 * (not thread class loader level). <br />
	 * Prerequisite and importunate reminder: 
	 * <ul>
	 * <li>Only supports Sun's JVM (currently).</li>
	 * <li>JVM spec version needs to be ver. 1.5 or higher. This condition will be checked in this method 
	 * before attempting enabling specified JavaAgent.</li>
	 * <li>JDK's tools.jar (class.jar for Mac) needs to be in class path in order to enable JavaAgent.</li>
	 * <li>Specified JavaAgent does not work most likely for the classes what have 
	 * been already loaded before calling this method (unless they will be reloaded.)</li>
	 * <li>super class of Agent (if there is) needs to have been loaded to system class 
	 * loader before.</li>
	 * <li>be mindful of class loader tanglement after dynamically enabling JavaAgent.</li>
	 * </ul> 
	 * @param agentFilePath path to agent library (.jar) file.
	 */
	public void enableJavaAgent( final String agentFilePath, final String agentParameters) {
		String agentParametersCopy = ((agentParameters == null) ? "" : agentParameters.trim());
		
		// Validation on agentFilePath ------------------------------------------------------------
		String agentFilePathCopy = (agentFilePath == null) ? "" : agentFilePath.trim();
			if ( agentFilePathCopy.length() < 1) {
				throw new IllegalArgumentException( 
						String.format(
								"Given path to javaagent file is not valid string as a path: %1$s",
								((agentFilePath == null) ? "null" : ("\"" + agentFilePath + "\""))
								)
						);
			}
			File agentFile = new File( agentFilePathCopy);
				if ( !agentFile.isFile()) {
					throw new IllegalArgumentException( 
							String.format(
									"Given path to javaagent file does not point to a .jar file: %1$s",
									agentFilePathCopy
									)
							);
				}
			try {
				JarFile agentJarFile = new JarFile( agentFilePathCopy);
				Manifest manifest = agentJarFile.getManifest();
					if ( manifest == null) {
						throw new IllegalArgumentException( 
								String.format(
										"%1$s does not contain MANIFEST.MF file (with Agent-Class attribute " +
										"value.)",
										agentFilePathCopy
										)
								);
					}
//TODO is it possible to have multiple Agent-Class and/or Premain-Class entries in single MANIFEST.MF? If so, then rather start from manifest.getEntries()
				Attributes mainAttributes = manifest.getMainAttributes();
				String agentClassStr = mainAttributes.getValue( "Agent-Class");
					if ( agentClassStr == null) {
						agentClassStr = mainAttributes.getValue( "Premain-Class");
					}
					if ( agentClassStr == null) {
						throw new IllegalArgumentException( 
								String.format(
										"Detected no entry for Agent-Class or Premain-Class attribute in " 
										+ "MANIFEST.MF file of %1$s.",
										agentFilePath
										)
								);
					}
			}
			catch( Exception exception) {
				if ( exception instanceof RuntimeException) throw (RuntimeException)exception;
				throw new RuntimeException(
						String.format(
								"Failure in validating %1$s as a .jar file.",
								agentFilePathCopy
								),
						exception
						);
			}
		// ----------------------------------------------------------------------------------------
		
		if ( !isApplicableJavaVersion()) {
			throw new UnsupportedOperationException( 
					String.format(
							"Spec version of JVM needs to be 1.5 or higher but current one is %1$s.",
							getJvmSpecVersion()
							)
					);
		}
			
			
		String nameOfRunningVM = ManagementFactory.getRuntimeMXBean().getName();
		int p = nameOfRunningVM.indexOf( '@');
		String pid = nameOfRunningVM.substring( 0, p);
		
		if ( logger.isLoggable( Level.FINE)) {
			logger.log(
					Level.FINE,
					String.format(
							"Going to load %1$s and dynamically apply JavaAgent from it to " +
							"current process (process ID: %2$s).",
							agentFilePathCopy, 
							pid
							)
					);
		}
		try {
			VirtualMachine virtualMachine = VirtualMachine.attach( pid);
			if ( agentParametersCopy.length() < 1) {
				virtualMachine.loadAgent( agentFilePathCopy);
			}
			else {
				virtualMachine.loadAgent( agentFilePathCopy, agentParametersCopy);
			}
			virtualMachine.detach();
		}
		catch( Exception exception) {
			throw new RuntimeException(
					String.format(
							"Failure in dynamically applying JavaAgent from %1$s to current process " 
							+ "(process ID: %2$s)",
							agentFilePathCopy, 
							pid
							),
					exception
					);
		}
		if ( logger.isLoggable( Level.FINE)) {
			logger.log(
					Level.FINE, 
					String.format(
							"JavaAgent in %1$s is now applied to current process (process ID: %2$s).",
							agentFilePathCopy, 
							pid
							)
					);
		}
	}
}
